開始講到單例狀態就要稍微舉例一下什麼是單例狀態,其實就是泛指所有 Singleton 的物件。舉凡整個應用程式只有唯一的狀態,便都是這種狀態。比如「History Router」、「Locale Language」、「Browser Storage」都算是。這類的狀態不但可以進行微前端溝通,還會產生副作用。如果沒有良好的控制管理,都會引發許多狀態管理的麻煩。
單例狀態之所以是單例狀態,就是整個 global 之下只有一個 因為你會發現很容易發生「循環修改」,資料流的混亂。你根本搞不清楚誰先誰後,誰頭誰尾。
要控制資料流並不簡單,因為很容易發生資料被到處修改的問題,你也很難管理團隊或套件去修改 global browser API。但能做的基本還是要做,建立狀態的抽象介面,統一 Side Effect 的資料流流向,這才能最好的去控制資料能夠集中。
// router.js
export function navigation(path) {
history.pushState({}, "", path);
window.dispatchEvent(new Event("navigation"));
}
import { navigation } from "./router";
function RouterLink() {
const onClick = (path) => {
navigation(path);
};
return <button onClick={onClick} />;
}
這樣整個專案都可以透過 navigation
來達到改變 history
的狀態,也可以使用 dispatchEvent 去和不同的微前端應用程式進行同步。
但終究是微前端,多個應用下資料依然是不同步,並不是完美的單向資料流。此時可以利用 Event 製作一個主動觸發的行為,形成一個單一 Provider 的事件觸發器。
// init.js
export function init() {
window.addEventListener("navigation", (event) => {
history.pushState({}, "", event.detail.path);
});
}
// router.js
export function navigation(path) {
window.dispatchEvent(
new CustomEvent("navigation", {
detail: { path },
})
);
}
import { navigation } from "./router";
function RouterLink() {
const onClick = (path) => {
navigation(path);
};
return <button onClick={onClick} />;
}
這樣只要確保 init
在應用程式初始化時只執行一次,那全部的微前端都可以使用 navigation
方法來進行 Side Effect。
又舉例,如果想要攔截本身 window 自帶的方法怎麼辦?
const originOpen = window.open;
window.open = function (...args) {
originOpen.apply(this, args);
// implements
};
const prototypePush = Array.prototype.push;
Array.prototype.push = function (...args) {
prototypePush.apply(this, args);
// implements
};
透過替換掉原本的方法,這樣就可以控管攔截後的結果,如此一來就可以對一些 Library 底層行為進行攔截或改變。
在微前端架構中,路由器(Router)是不得不面對,而且情境極複雜的一個議題。
因為 browser history 的對象在整個前端應用程式是唯一的,不會存在第二個。但如果有多的前端應用程式產生出多個 history 的操作器,那高機率就要打架了。在整個運作系統之中,最怕的就是「改了不動」、「改了不知道」、「遞迴循環修改」等情境。
其實看過各大框架系統,都不太推薦微前端模組另外開自己的路由系統,多半鼓勵以「原子化元件」作為切分點。如果以元件切分,確實路由議題可以全然仰賴上層主應用控制,它本身的渲染控制便不在元件邏輯內。
但相信做過微前端的都知道,你每個微前端切分成本很大,會希望一個應用包裝更多功能而不要切分,這就延伸新的問題「路由器的狀態同步」。狀態同步議題其實回到上一篇講的「單例狀態管理」,解決方案是一致的。
我這邊最推薦的就是微前端應用「不要操作路由器」,那不操作路由器該怎麼操作與通知?其實是一樣的解法,採用單向資料流的方式統一管理,讓可以改變狀態的窗口全部一致在某個觸發點,不能全世界好幾個地方能改。當單向資料流徹底管理後,就可以很容易去攔截所有路由的副作用,如此一來就可以建構狀態管理系統去「取得」、「修改」、「監聽」,行形成一套資料流。
先說明 HttpFetcher 是什麼。當前主流的 HttpFetcher 應該就是 axios, fetch ... 等等方式,凡是向網路發出請求的方法都是 HttpFetcher 相關的工具。
大部分我們在處理 Http Request 時,都會有一些「固定要做的事」。精準來說要舉例就是一些共同的處理,比如說將 Token 注入、請求的錯誤處理、回應的錯誤處理、回應的處理等等,相關的中介層行為肯定不會想重複編寫,大部分行為對於一個網站也幾乎是一致的,不太會有不同頁有不同的處理。所以在微前端的架構如何去共用這個行為呢?
既然是 HTTP 中介層,使用的處理行為就未必要在瀏覽器進行完成,你可以選擇在伺服器進行請求處理,如 Token 管理就可以在伺服器端完成。
透過層層事件把行為透過 EventBus 傳遞給每一層事件處理,算是另類的 middleware,有點 Chain of Responsibility Pattern
的感覺。除了可以用 deep 的方式去深層傳遞,也可以用 Next Event
的手法去幫助層層傳遞。EventBus 更方便讓微前端之間進行溝通與攔截,達到更高的自由度。
這方式就是建立一個抽象實體去讓全部請求共用,管理這一份記憶體也可以很好控制所有 Interceptor 行為。
強行攔截所有 HTTP 請求,重新封裝 XHR 與 Fetch 行為,在 ServiceWork 重新處理請求行為。
直接放棄所有共用行為,每一個微前端應用程式都獨立執行對應的行為。
其實全域狀態還有很多,方案也許也很多,我也只提供初步的作法,事實上要優化的部分還非常多,礙於篇幅就不特別再多說。